2

观察者模式,也叫订阅-发布模式。顾名思义,就是订阅某些功能,然后在适当的时机发布出来,也就是执行这些功能。
订阅:就是把几个函数推入数组中待用;
发布:就是把缓存在数组中的函数拿出来执行;

var login = {};
login.eventList = {};
//将函数推入数组中保存,待用
login.listen = function(key, fn) {
    if(!this.eventList[key]) {
        this.eventList[key] = [];
    }
    this.eventList[key].push(fn);
}
login.trigger = function(key) {
    var fns = this.eventList[key];
    if(!fns || fns.length === 0) {
        return false;
    }
    for(var i=0; i<fns.length;i++) {
        fns[i]();
    }
}

//订阅
login.listen('loginSuccess', function() {
    console.log('显示用户头像');
})
login.listen('loginSuccess', function() {
    console.log('显示消息列表');
})

//发布
login.trigger('loginSuccess');

应用场景:
现在前端领域,SPA单页应用已经非常普遍了,每个页面,都是用ajax异步请求。ajax请求有一个比较闹心的问题,就是层级回调。比如:
有一个页面,需要调用三个数据接口。
第一个是login登录接口,
第二个是根据登录接口返回的id,调取头像接口。
第三个是根据登录接口返回的id,调取消息列表接口。

一般情况下会这么写:

$.ajax({
    url: 'http://ajax.login.com',
    dataType: 'json',
    success: function(data) {
        getAvatar(data.id);
        getMsg(data.id);
        ...
    }
})

这样写虽然没有问题,但却不容易维护。如果哪天改了需求,需要加个接口,你还得翻出这段代码,找到success回调,再加上一个函数。加函数还算好的,有的人会直接在success回调里继续写$.ajax这样的代码,一级一级的这么摞着写,这样代码很快就会变成一堆大便,变得不可维护。这种写法叫做造粪模式,百分百的造出垃圾来。因为耦合性太大,接口调用都成了拴在一条绳子上的蚂蚱,一扯就是一坨。
如何解耦呢?就是利用订阅发布模式,我们可以在getAvatar方法中,订阅(listen)login接口,而一旦login接口走到success回调,我们就发布(trigger)一下

var event = {
    eventList: {},
    listen: function(key, fn) {
        if(!this.eventList[key]) {
            this.eventList[key] = [];
        }
        this.eventList[key].push(fn);
    },
    trigger: function() {
        var key = Array.prototype.shift.call(arguments);
        var fns = this.eventList[key];
        if(!fns || fns.length === 0) {
            return false;
        }

        for(var i=0; i<fns.length; i++) {
            fns[i].apply(this, arguments);
        }
    }
};

var installEvent = function(obj) {
    //浅拷贝
    for(var i in event) {
        obj[i] = event[i];
    }
}
var login = {};
installEvent(login);

//订阅
login.listen('loginSuccess', function() {
    console.log('显示用户头像');
});
login.listen('loginSuccess', function() {
    console.log('显示消息列表');
});
//发布
login.trigger('loginSuccess');

现在订阅没有问题了,那如何取消订阅呢?我们再加上取消订阅函数

var event = {
    eventList: {},
    listen: function(key, fn) {
        if(!this.eventList[key]) {
            this.eventList[key] = [];
        }
        this.eventList[key].push(fn);
    },
    remove: function(key, fn) {
        var fns = this.eventList[key];
        if(!fns) {
            return false;
        }
        if(!fn) {
            //如果没有回调,表示取消此key下的所有方法
            fns && (fns.length);
        } else {
            for(var i=0; i<fns.length; i++) {
                if(fns[i] == fn) {
                    fns.splice(i, 1);
                }
            }
        }
    },
    trigger: function() {
        var key = Array.prototype.shift.call(arguments);
        var fns = this.eventList[key];
        if(!fns || fns.length === 0) {
            reutrn false;
        }
        for(var i=0; i<fns.length; i++) {
            fns[i].apply(this, arguments);
        }
    }
};

var installEvent = function(obj) {
    for(var i in event) {
        obj[i] = event[i];
    }
};

var login = {};
installEvent(login);

//显示用户头像
function showAvatar() {
    console.log('显示头像数据');
};

//显示消息列表
function showMessage() {
    console.log('显示消息列表');
};

//订阅
login.listen('loginSuccess', showAvatar);
login.listen('loginSuccess', showMessage);

//发布
login.trigger('loginSuccess');

//取消订阅
login.remove('loginSuccess', showAvatar);

//再次发布
login.trigger('loginSuccess');

我们的订阅发布模式走到这里,基本上已经完善了。最后我们来看一下ajax回调问题怎么来解决。我们其实根本不需要在登录的ajax回调中加拉取头像等逻辑,而只需让拉取头像功能订阅登录接口即可,当登录工作完成后会发布,也就是触发缓存在数组中的函数执行。

<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>订阅-观察者模式</title>
<script src="http://mockjs.com/dist/mock.js"></script>
<script src="http://cdn.bootcss.com/jquery/1.11.3/jquery.js"></script>
</head>
<body>
</body>
<script>
  Mock.mock('http://ajax.login.com', {
      'name': '@name',
      'age|1-100': 1
  });
  var event = {
      eventList: {},
      listen: function(key, fn) {
          if(!this.eventList[key]) {
              this.eventList[key] = [];
          }
          this.eventList[key].push(fn);
      },
      remove: function(key, fn) {
          var fns = this.eventList[key];
          if(!fns) {
              return false;
          }
          if(!fn) {
              fns && fns.length = 0;
          } else {
              for(var i=0; i<fns.length; i++) {
                  if(fn == fns[i]) {
                      fns.splice(i, 1);
                  }
              }
          }
      },
      trigger: function() {
          var key = Array.prototype.shift.call(arguments);
          var fns = this.eventList[key];
          if(!fns || fns.length === 0) {
              return false;
          }
          for(var i=0; i<fns.length; i++) {
              fns[i].apply(this, arguments);
          }
      }
  };

  var installEvent = function(obj) {
        for(var i in event) {
              obj[i] = event[i];
        }
  };

  var login = {};
  installEvent(obj);

  var avatar = (function() {
      login.listen('loginSucc', function() {
            avatar.setAvatar(data);
      });

      return {
          setAvatar: function(data) {
                console.log('显示用户' + data['name'] + '的头像');
          }
      }
  })();

  var message = (function() {
        login.listen('loginSucc', function(data) {
          message.setMsg(data);
        });

        return {
              setMsg: function(data) {
                    setTimeout(function() {
                          console.log('显示用户' + data['name'] + '的消息');
                    })
              }
        }
  })();
  
  //发布
  $.ajax({
        url: 'http://ajax.login.com',
        dataType: 'json',
        success: function(data) {
              login.trigger('loginSucc', data);
        }
  })
</script>
<html>

事实上,还有一种更普遍意义的订阅发布模式。比如在一个按钮上绑定click事件,这其实就是一个订阅的过程;而鼠标点击就是发布。


小被子
839 声望10 粉丝